Reduce syscall and scheduler overhead in socket accept and close - #11490
Reduce syscall and scheduler overhead in socket accept and close#11490lpgauth wants to merge 4 commits into
Conversation
close() can only block when SO_LINGER is set with a positive timeout, but nif_finalize_close always runs on a dirty scheduler, costing two scheduler migrations per closed socket. Check the linger option and only take the dirty path when close can block.
CT Test Results 3 files 136 suites 50m 50s ⏱️ Results for commit 9b0fdcf. ♻️ This comment has been updated with latest results. To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass. See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally. Artifacts// Erlang/OTP Github Action Bot |
Passing SOCK_NONBLOCK to accept4() makes the SET_NONBLOCKING call on every accepted socket redundant, saving a syscall per accept on platforms that have accept4().
Every setsockopt went through a save and restore of both SO_PRIORITY and IP_TOS, on the grounds that "if any other option is set after tos, tos might be zeroed" - a comment carried over from the inet driver. That costs two getsockopt calls and up to two more setsockopt calls on every option set. Only half of it holds. Setting IP_TOS does make the kernel derive a new SO_PRIORITY from it, so that one option has to put the priority back. Setting SO_PRIORITY leaves the tos alone, and so does setting anything else: verified on Linux 7.0 with TCP_NODELAY, SO_RCVBUF and SO_LINGER, none of which disturbed either value. So do it only for IP_TOS, and only for the priority. Setting an option on an accepted connection goes from four system calls to one. The socket options a connection sets still look independent to the user, which is what the dance was there for.
a48655d to
6be6a8e
Compare
|
These patches have been running in production for several days with no issues. I can share more benchmark numbers if that's useful. |
| if (descP->sock == INVALID_SOCKET) | ||
| return FALSE; | ||
|
|
||
| if (sock_getopt(descP->sock, SOL_SOCKET, SO_LINGER, |
There was a problem hiding this comment.
What if SO_LINGER is not defined on a given platform? I see other places in this file ifdef-ing on the macro's existence.
There was a problem hiding this comment.
Fixed in 9b0fdcf. When the macro is missing the close can't linger, so it skips the dirty scheduler. Also switched the length to SOCKOPTLEN_T while at it, SOCKLEN_T falls back to size_t on Windows where getsockopt wants an int.
SO_LINGER is guarded everywhere else in the file, so guard the new check too. Without the option a close cannot linger, so the dirty scheduler is skipped. Also use SOCKOPTLEN_T for the option length like every other getsockopt call in the file; SOCKLEN_T falls back to size_t on Windows where getsockopt writes through an int pointer.
|
Pushed the SO_LINGER guard as a separate commit, will squash before merge. Also worth noting the Windows and OpenBSD CI failures here aren't related to the change: the Windows job died setting up wxWidgets before compiling anything, and the OpenBSD VM hung mid-build in megaco. |
nif_finalize_closealways runs on a dirty scheduler, but close() canonly block when
SO_LINGERis set with a positive timeout, so everyclosed socket pays two scheduler migrations for a case that rarely
applies. Check linger first and only go dirty when the close can
actually block. If the check races with a concurrent close the
getsockopt fails and we just fall back to the dirty path.
The second commit passes
SOCK_NONBLOCKto accept4() so theSET_NONBLOCKINGcall on every accepted socket can be skipped.Quick benchmark, HTTP server on the socket backend, 64 concurrent
connections, one request per connection (M2 Pro): 21.6k -> 24.3k req/s.
Keep-alive throughput unchanged. No accept4 on macOS, so that's the
close change alone.
socket_SUITE and socket_api_SUITE pass on macOS, both maint and master.